package org.dei.dataserver;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

import org.dei.perla.utils.dataconverter.ArrayUtility;
import org.dei.perla.utils.messages.AdapterFpcMessage;
import org.dei.perla.utils.pipe.Pipe;
import org.dei.perla.utils.pipe.PipePair;
import org.dei.perla.utils.waiter.Waitable;
import org.dei.perla.utils.waiter.Waiter;

public class Server extends Thread {
	
	private static final char SEND_COMMAND = 's';
	private static final char UPDATE_COMMAND = 'p';
	private static final char ACK = 'a';
	private static final byte DEV_NUMBER = 4;
	private static final String BASE_FILE_NAME = "./mem_id_";
	private static final String FILE_EXTENSION = ".dat";
	
	private Byte[][] updateData;
	private ServerSocket serverSocket = null;
	
	private ArrayList<Integer> fpcIdPipesIndex;
	private ArrayList<Pipe<AdapterFpcMessage>> fpcInPipes;
	private ArrayList<Pipe<AdapterFpcMessage>> fpcOutPipes;
	private Waiter<Pipe<AdapterFpcMessage>> inputPipesWaiter;
	
	private Thread fpcListenerThread;
	
	/**
	 * Costruttore
	 */
	public Server(int port) {
		updateData = new Byte[DEV_NUMBER][];
		fpcInPipes = new ArrayList<Pipe<AdapterFpcMessage>>();
		fpcOutPipes = new ArrayList<Pipe<AdapterFpcMessage>>();
		fpcIdPipesIndex = new ArrayList<Integer>();  
		inputPipesWaiter = new Waiter<Pipe<AdapterFpcMessage>>();
		inputPipesWaiter.start();
		InetSocketAddress sockAddr;
		
		sockAddr = new InetSocketAddress(port);
		
		try {
			serverSocket = new ServerSocket();
			serverSocket.setReuseAddress(true);
			serverSocket.bind(sockAddr);
		} catch (IOException e) {
			System.out.println("Error listening on port 5606!");
			e.printStackTrace();
			System.exit(1);
		}
		
		fpcListenerThread = new Thread() {
			
			public void run() {
				
				Waitable<Pipe<AdapterFpcMessage>> waitable;
				Pipe<AdapterFpcMessage> tmpPipe;
				int fpcId;
				
				do {
					while((waitable = inputPipesWaiter.waitNext()) != null) {
						System.out.println("Updating data on pipe...");
						tmpPipe = waitable.getParentComponent();
						fpcId = fpcIdPipesIndex.get(fpcInPipes.indexOf(tmpPipe));
						setUpdateData(fpcId, ArrayUtility.toByteClassArray(tmpPipe.dequeue().getPayload()));
					}
					
					//TODO: se si arriva qui vuol dire che una fpc e' terminata ed ha chiuso la pipe
					// devo ciclare le varie pipes per vedere quale e' stata chiusa
				} while(true);
			}
		};
		fpcListenerThread.start();
	}
	
	/**
	 * Genera o recupera una coppia di pipes per permettere la comunicazione con l'FPC
	 * @param fpcID numero identificativo dell'FPC (corrisponde con il canale virtuale)
	 * @return coppia di pipes da utilizzare per la comunicazione con il server
	 */
	public synchronized PipePair<AdapterFpcMessage, AdapterFpcMessage> getFpcPipes(int fpcID) {
		
		int pipesIndex = fpcIdPipesIndex.indexOf(fpcID);
		PipePair<AdapterFpcMessage, AdapterFpcMessage> pipePair;
		
		if (pipesIndex == -1) {
			pipePair = new PipePair<AdapterFpcMessage, AdapterFpcMessage>( 
					new Pipe<AdapterFpcMessage>("FPC " + fpcID + "InPipe"), 
					new Pipe<AdapterFpcMessage>("FPC " + fpcID + "OutPipe"));
			
			inputPipesWaiter.addWaitable(pipePair.getOutPipe().getWaitable());
			pipePair.getInPipe().start();
			pipePair.getOutPipe().start();
			fpcOutPipes.add(pipePair.getInPipe());
			fpcInPipes.add(pipePair.getOutPipe());
			fpcIdPipesIndex.add(fpcID);
			
			return pipePair;
		} else {
			return pipePair = new PipePair<AdapterFpcMessage, AdapterFpcMessage>(
					fpcOutPipes.get(pipesIndex), fpcInPipes.get(pipesIndex));
		}
	}
	
	/**
	 * Imposta i dati da inviare verso il dispositivo. I dati verranno inviati 
	 * quando il nodo eseguira' una richiesta di aggiornamento dei parametri
	 * 
	 * @param deviceNumber numero del dispositivo da aggiornare
	 * @param data classe GenericStructure contenente i dati da inviare al dispositivo
	 */
	public synchronized void setUpdateData(int deviceNumber, Byte[] data) {
		
		if (deviceNumber < DEV_NUMBER) {
			updateData[deviceNumber] = data;
		}
	}
	
	/**
	 * Accetta una connessione remota ed esegue la ricezione dei dati
	 * o invia gli aggiornamenti ai nodi
	 */
	public void acceptConnection() {
		
		Socket clientSocket = null;
		DataInputStream dIn = null;
		DataOutputStream dOut = null;
		
		try {
			System.out.println("Accepting...");
			clientSocket = serverSocket.accept();
			System.out.println("Connection accepted!");
		} catch (IOException e) {
			System.out.println("Error accepting incoming connection!");
			e.printStackTrace();
		}
		
		try {
			dIn = new DataInputStream(clientSocket.getInputStream());
			dOut = new DataOutputStream(clientSocket.getOutputStream());
		} catch (IOException e) {
			System.out.println("Error setting input and output streams!");
			e.printStackTrace();
		}
		
		try {
			char command = (char)dIn.read();
			
			if (command == SEND_COMMAND) {
				System.out.println("Send command received!");
				receiveMessage(dIn, dOut);
				clientSocket.close();
			} else if (command == UPDATE_COMMAND) {
				System.out.println("Update command received!");
				sendUpdate(dIn, dOut);
				clientSocket.close();
			}
		} catch (IOException e) {
			System.out.println("Error executing the command!");
			e.printStackTrace();
		}
	}
	
	/**
	 * Esegue la ricezione dei dati dal nodo
	 * 
	 * @param dIn DataInputStream da cui ricevere i dati dalla socket
	 * @param dOut DataOutputStream su cui inviare i ddati
	 */
	private void receiveMessage(DataInputStream dIn, DataOutputStream dOut) {
		 byte tDevNumber = 0;
		 byte blobData[];
		 long fileSize = 0;
		 FileOutputStream fOut;
		 File file;
		
		try {
			tDevNumber = (byte)dIn.read();
			fileSize += (dIn.read() & 0xFF) << 0;
			fileSize += (dIn.read() & 0xFF) << 8;
			fileSize += (dIn.read() & 0xFF) << 16;
			fileSize += (dIn.read() & 0xFF) << 24;
			System.out.println("Preparing to receive the file number " + tDevNumber + ", of size " + fileSize);
			System.out.println("Sending ACK...");
			dOut.write((byte)ACK);
			file = new File(BASE_FILE_NAME + tDevNumber + FILE_EXTENSION);
			fOut = new FileOutputStream(file);
			
			System.out.println("Writing to file...");
			int comma = 1;
			blobData = new byte[(int)fileSize];
			int i = 0;
			while (fileSize != 0) {
				blobData[i] = dIn.readByte();
				fOut.write(blobData[i++]);
				fileSize--;
				if ((comma++ % 2) == 0) {
					fOut.write(',');
				}
			}
			fOut.close();
			System.out.println("Writing done!");
			AdapterFpcMessage tMsg = new AdapterFpcMessage();
			tMsg.setPayload(blobData);
			fpcOutPipes.get(fpcIdPipesIndex.get(tDevNumber)).enqueue(tMsg);
			
		} catch (IOException e) {
			System.out.println("Error reading the file or the file specs!");
			e.printStackTrace();
		}
	}
	
	/**
	 * Esegue l'invio dei parametri aggiornati al nodo (se disponibili)
	 * 
	 * @param dIn DataInputStream per la lettura da socket
	 * @param dOut DataOutputStream per la scrittura su socket
	 */
	private synchronized void sendUpdate(DataInputStream dIn, DataOutputStream dOut) {
		int devToUpdate;
		int updateLen;
		
		try {
			devToUpdate = 0;
			for (int i = 0; i < DEV_NUMBER; i++) {
				if (updateData[i] != null) {
					devToUpdate++;
				}
			}
			System.out.println("Sending the number of devices to update (" + devToUpdate + ")...");
			dOut.write((byte)(devToUpdate & 0xFF));
			if (dIn.read() != ACK) {
				return;
			}
			System.out.println("ACK received!");
			if (devToUpdate != 0) {
				for (int i = 0; i < DEV_NUMBER; i++) {
					if (updateData[i] != null) {
						System.out.println("Sending update for device " + i);
						updateLen = updateData[i].length;
						dOut.write(i & 0xFF); // Invio numero del dispositivo da aggiornare
						dOut.write((byte)(updateLen & 0xFF));
						dOut.write((byte)((updateLen >>> 8) & 0xFF));
						dOut.write((byte)((updateLen >>> 16) & 0xFF));
						dOut.write((byte)((updateLen >>> 24) & 0xFF));
						if (dIn.read() != ACK) {
							return;
						}
						System.out.println("ACK received!");
						for (int j = 0; j < updateData[i].length; j++) {
							dOut.write((byte)(updateData[i][j] & 0xFF));
						}
						updateData[i] = null;
						
						if (dIn.read() != ACK) {
							return;
						}
					}
				}
			}
			
		} catch (IOException e) {
			System.out.println("Error updating the parameters!");
			e.printStackTrace();
		}
	}
	
	/**
	 * Metodo run per l'esecuzione del ciclo di funzionamento del thread
	 */
	public void run() {
		while(true) {
			acceptConnection();
		}
	}
}
